useReducer
🧑🏻💻 useReducer
useReducer
는 컴포넌트에 reducer를 추가할 수 있는 React Hook이다.
✅ useReducer 문법
const [state, dispatch] = useReducer(reducer, initialArg, init?)
import { useReducer } from 'react';
// state 업데이트 로직을 컴포넌트 외부의 reducer 함수로 분리한 경우
function reducer(state, action) {
// ...
}
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
// ...
useReducer
는useState
와 유사한 방식으로 현재 state를 제공하고, 업데이트하는dispatch
함수를 반환한다.reducer
는 state가 업데이트되는 방식을 지정하는 함수이다.initialArg
는 초기 state가 계산되는 값이다.init
은 초기 state 계산 방법을 지정하는 초기화 함수이다.- 이것을 지정하지 않으면 초기 state는
initialArg
로 설정되고 초기 state는init(initialArg)
를 호출한 결과로 설정된다.
- 이것을 지정하지 않으면 초기 state는
🧑🏻💻 알고 가기
✅ dispatch 함수
- state를 다른 값으로 업데이트하고 리렌더링한다.
- 매개변수로
action
을 전달받고 반환값은 없다.action
은 사용자가 수행한 작업이다. 보통 type 속성이 있는 객체가 온다.const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
✅ reducer 함수
state를 어떻게 변경할지 action 로직을 작성하는 함수다. 보통 type을 활용한 if 문과 switch case 문으로 로직을 작성한다.
이때 state는 읽기 전용으로, 배열과 객체 state의 경우 변이하지 않고 교체해야 한다. 이를 신경 쓰기 싫다면
useImmerReducer
를 사용하자.
🧑🏻💻 활용 및 생각할 거리
✅ 초기 state 재생성 방지
React는 초기 state를 한 번 저장하고 다음 렌더링에서 이를 무시한다.
함수를 활용하여
initialArg
를 설정하고 싶다면 불필요한 함수 호출 방지를 위해 세번 째 인수에 초기화 함수를 전달하자.function createInitialState(username) {
// ...
}
function TodoList({ username }) {
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
✅ useState
vs. useReducer
- 코드 크기
useState
를 사용하면 미리 작성해야 하는 코드가 줄어든다.useReducer
를 사용하면 reducer 함수와 action을 전달하는 부분 모두 작성해야 한다.
→ 하지만 많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우useReducer
가 코드를 줄이는 데 도움이 될 수 있다.
- 가독성
간단한 state 업데이트에는
useState
가 좋다.구조가 복잡한 state 업데이트에는
useReducer
가 좋다.
→useReducer
를 사용하면 업데이트 로직이 어떻게 동작 하는지와 이벤트 핸들러를 통해 무엇이 일어났는지를 깔끔하게 분리할 수 있다.
- 디버깅
useState
는 디버깅이 어렵다.useReducer
는 reducer에 콘솔 로그를 추가하여 모든 state 업데이트에 대한 버그를 한 눈에 확인할 수 있다. → 각action
이 정확하다면, 버그가 reducer 로직 자체에 있다는 것을 쉽게 판단할 수 있다.
- 테스팅
useReducer
는useState
와 매우 유사하지만 컴포넌트에 의존하지 않는 순수한 함수이기 때문에, 이벤트 핸들러의 state 업데이트 로직을 컴포넌트 외부의 단일 함수로 옮길 수 있고 이를 테스트에도 사용할 수 있다.
✅ useState 대신 useReducer을 사용하면 좋은 경우
많은 이벤트 핸들러가 비슷한 방식으로 state를 업데이트하는 경우 useReducer을 사용하면 코드를 줄일 수 있다.
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
const reset = () => {
setCount(0);
};
return (
<div>
Count: {count}
<button onClick={increment}>증가</button>
<button onClick={decrement}>감소</button>
<button onClick={reset}>리셋</button>
</div>
);
};
export default Counter;
다음 코드에서 하나의 state에 대해서 많은 eventHandler 함수들을 만들어 내야 한다. 하지만 reducer을 사용할 경우 이러한 부분들을 더 명료하게 작성할 수 있다.
import React, { useReducer } from 'react';
// 초기 상태 설정
const initialState = {
count: 0,
};
// 리듀서 함수
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'reset':
return initialState;
default:
throw new Error();
}
};
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
Count: {state.count}
<button onClick={() => dispatch({ type: 'increment' })}>증가</button>
<button onClick={() => dispatch({ type: 'decrement' })}>감소</button>
<button onClick={() => dispatch({ type: 'reset' })}>리셋</button>
</div>
);
};
export default Counter;이처럼 reducer 함수를 정의하고 dispatch 함수를 통해 가독성을 높이고 개발자 의도가 잘 드러나는 코드를 작성할 수 있다.
이때 {…state, …}에서 …state를 spread operator로 펼쳐주는 이유는 state는 불변성을 유지하기 위함이다. 객체를 복사해서 참조 값으로 넣어주기 때문에 원본 객체를 손상시키지 않고 참조 값을 변경해 줄 수 있다.